/*
**************************************************************************************
*Myriad Engine                                                                       *
*Copyright (C) 2006-2007, 5d Studios (www.5d-Studios.com)                            *
*                                                                                    *
*This library is free software; you can redistribute it and/or                       *
*modify it under the terms of the GNU Lesser General Public                          *
*License as published by the Free Software Foundation; either                        *
*version 2.1 of the License, or (at your option) any later version.                  *
*                                                                                    *
*This library is distributed in the hope that it will be useful,                     *
*but WITHOUT ANY WARRANTY; without even the implied warranty of                      *
*MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU                   *
*Lesser General Public License for more details.                                     *
*                                                                                    *
*You should have received a copy of the GNU Lesser General Public                    *
*License along with this library; if not, write to the Free Software                 *
*Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA      *
**************************************************************************************
*/
package leola.backend.interpreter;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

import leola.backend.EvalException;
import leola.types.LeoFunction;
import leola.types.LeoNativeClass;
import leola.types.LeoNativeFunction;
import leola.types.LeoNull;
import leola.types.LeoObject;
import leola.utils.ClassUtil;
import leola.utils.Pair;

/**
 * Executes the function types 
 * 
 * @author Tony
 *
 */
public class FunctionExecutor {

	/**
	 * Interpreter
	 */
	private Interpreter interpreter;
		
	/**
	 * @param interpreter
	 */
	public FunctionExecutor(Interpreter interpreter) {
		this.interpreter = interpreter;
	}
	
	/**
	 * Executes a function
	 * 
	 * @param function
	 * @param params
	 * @throws Exception
	 */
	public void executeFunction(LeoFunction function, LeoObject ... params) throws Exception {
		LeoObject parameters[] = params;
		
		
		String[] paramNames = function.getParameters();
		int numberOfArgs = paramNames.length;
		
		if ( params.length < numberOfArgs ) { /* fill out any missing parameters with NULLS */
			LeoObject paramsWithNulls[] = new LeoObject[numberOfArgs];
			System.arraycopy(params, 0, paramsWithNulls, 0, params.length);
			Arrays.fill(paramsWithNulls, params.length, numberOfArgs, LeoNull.LEONULL);
			
			parameters = paramsWithNulls;
		}
		else if ( params.length > numberOfArgs ) {
			throw new EvalException
				("The function: " + function + " does not contain " + params.length + " number of parameters.");
		}
		
		SymbolStack stack = this.interpreter.getSymbolStack();
		
		Scope funcScope = new Scope(true);
		stack.push(function.getScope()); // the parent scope for closures
		stack.push(funcScope); // the functions scope
		{
			for(int i = 0; i < parameters.length; i++) {
				funcScope.getSymbolTable().put(paramNames[i], parameters[i]);
			}
					
			function.getBody().visit(this.interpreter);
		}
		stack.pop();
		stack.pop();
	}

	
	/**
	 * Executes a native function with unknown parameters.
	 * 
	 * @param owner
	 * @param functionName
	 * @param params
	 * @throws Exception
	 */
	public void executeUnknownNativeFunction(LeoNativeClass owner
											, String functionName											
											, LeoObject ... params) throws Exception {
		
		Pair<Method, Object> result = ClassUtil.invokeMethod(owner.getNativeClass(), functionName
											 				, owner.getInstance(), params);
		
		
		if ( ! result.getFirst().getReturnType().equals(Void.class) ) {			
			Object methodResult = result.getSecond();
			this.interpreter.getCallstack().push( LeoTypeConverter.convertToLeolaType(methodResult) );
		}
	}
	
	/**
	 * Executes the native function.
	 * 
	 * @param function
	 * @param params
	 * @throws Exception
	 */
	public void executeNativeFunction(LeoNativeClass owner
									, String functionName
									, LeoNativeFunction function
									, LeoObject ... params) throws Exception {
		
		Object parameters[] = {};
				
		Class<?>[] paramTypes = function.getParamClasses();
		int numberOfArgs = paramTypes.length;
		
		if ( params.length == numberOfArgs 
				&& params.length > 0 ) { 
			
			/* convert to the correct java type to the best of our ability */
			ObjectArrayJavaTypeVisitor visitor = new ObjectArrayJavaTypeVisitor();
			
			Object paramsWithNulls[] = new Object[numberOfArgs];
			visitor.params = paramsWithNulls;
			
			for(int i = 0; i < numberOfArgs; i++ ) {
				visitor.currentIndex = i;								
				
				Class<?> type = paramTypes[i];
				LeoTypeConverter.convertLeoObjectToJavaObj(visitor, type, params[i]);
			}
					
			parameters = paramsWithNulls;			
		}		
		else if ( params.length > numberOfArgs ) {
			throw new EvalException
				("The function: " + function + " does not contain " 
						+ params.length + " number of parameters.");
		}
						
		invokeNative(owner, functionName, function, parameters);		
	}
	
	/**
	 * Invokes the native method.
	 * 
	 * @param owner
	 * @param functionName
	 * @param function
	 * @param parameters
	 * @return
	 * @throws Exception
	 */
	private void invokeNative(LeoNativeClass owner
							, String functionName
							, LeoNativeFunction function
							, Object parameters[]) throws Exception {
		
		Class<?> ownerClass = function.getOwnerClass();
		Method method = ownerClass.getMethod(function.getFunctionName(), function.getParamClasses());
		
		
		/* If there is an instance of the class include it */
		try {
			Object result = ( owner != null ) ? method.invoke(owner.getInstance(), parameters)
											  : method.invoke(null, parameters);
			
			
			if ( ! method.getReturnType().equals(Void.class) ) {			
				this.interpreter.getCallstack().push( LeoTypeConverter.convertToLeolaType(result) );
			}
		}
		catch(InvocationTargetException e) {
			throw new EvalException(e.getCause() + " - " + functionName + ":" + owner);
		}
		
	}
}
